﻿//---------------------------------------------------------------------------
// <summary>
// InfoPath extraction utility
// </summary>
//---------------------------------------------------------------------------

namespace InfoPathExtractionRules
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Text;
    using Microsoft.VisualStudio.TestTools.WebTesting;

    /// <summary>
    /// Utility functions used for extraction rules.
    /// </summary>
    public static class IPExtractionUtility
    {
        /// <summary>
        /// String used to identify the form ID.
        /// </summary>
        public const string InfoPathArrayFormsName = "_InfoPath.arrForms[_InfoPath.iCurrentFormIndex] = ";

        /// <summary>
        /// String used to identify the start of the canary cookie's key.
        /// </summary>
        public const string CanaryStart = "_InfoPath_CanaryValue";

        /// <summary>
        /// Default path used to identify an invocation or full page postback request.
        /// </summary>
        public const string DefaultFormServerPath = "/_layouts/FormServer.aspx";

        /// <summary>
        /// Path used to identify a partial postback request.
        /// </summary>
        public const string PartialPostBackPath = "/_layouts/Postback.FormServer.aspx";

        /// <summary>
        /// Name of the full page postback parameter for the InfoPath event log.
        /// </summary>
        public const string EventLogFormPostBackParameterName = "__EventLog";

        /// <summary>
        /// String used to match the current form data javascript array definition of a standalone form.
        /// </summary>
        private static string varName = "var g_objCurrentFormData_";

        /// <summary>
        /// Characters to trim when extracting data.
        /// </summary>
        private static char[] trimCharacters = " \"\r\n".ToCharArray();

        /// <summary>
        /// Characters to trim when extracting data.
        /// </summary>
        /// <returns>The characters that should be trimmed.</returns>
        public static char[] TrimCharacters()
        {
            return trimCharacters;
        }

        /// <summary>
        /// Construct the full value of the string used to match the current form data javascript array
        /// definition of a standalone form.
        /// </summary>
        /// <param name="responseBody">String body from a server HTTP response.</param>
        public static void UpdateVarName(string responseBody)
        {
            string var = ParseCurrentFormData(responseBody, InfoPathArrayFormsName, ";");
            StringBuilder sb = new StringBuilder(varName);
            sb.Append(var).Append(" = [");
            varName = sb.ToString();
        }

        /// <summary>
        /// Finds the javascript declaration for g_ObjCurrentFormData.
        /// </summary>
        /// <param name="body">String body from a server HTTP response.</param>
        /// <returns>String inside the brackets on RHS of the assignment to g_ObjCurrentFormData.</returns>
        public static string ParseCurrentFormData(string body)
        {
            UpdateVarName(body);
            string returnValue = ParseCurrentFormData(body, varName, "];");

            return returnValue;
        }

        /// <summary>
        /// Finds the javascript declaration for g_ObjCurrentFormData.
        /// </summary>
        /// <param name="body">String body from a server HTTP response.</param>
        /// <param name="startIndex">String used to get the starting index of its first occurence in the string body.</param>
        /// <param name="endIndex">String used to get the ending index of its first occurence in the string body.</param>
        /// <returns>String inside the brackets on RHS of the assignment to g_ObjCurrentFormData.</returns>
        public static string ParseCurrentFormData(string body, string startIndex, string endIndex)
        {
            // Find where the g_objCurrentFormData is defined in javascript
            int varStart = body.IndexOf(startIndex, StringComparison.Ordinal);

            // We're going to extract the substring after the endKey of the javascript var
            varStart += startIndex.Length;
            int varEnd = body.IndexOf(endIndex, varStart, StringComparison.Ordinal);
            int varLength = varEnd - varStart; // Not adding 1 to skip the close bracket
            string var = body.Substring(varStart, varLength);
            return var;
        }

        /// <summary>
        /// Finds javascript declarations for g_ObjCurrentFormData.
        /// Creates CurrentFormData object for each one.
        /// </summary>
        /// <param name="body">String body from a server HTTP response.</param>
        /// <returns>List of CurrentFormData.</returns>
        public static Collection<InfoPathExtractionRules.CurrentFormData> GetCurrentFormDataArrays(string body)
        {
            const string VarNameStart = "var g_objCurrentFormData_";
            const string VarNameFormControl = "_FormControl";
            const string Equals = " = [";

            // Holds the g_objCurrentFormData array definitions
            Collection<InfoPathExtractionRules.CurrentFormData> currentFormData = new Collection<InfoPathExtractionRules.CurrentFormData>();

            int varNameStart = 0;
            int varNameEnd = 0;
            string varName;

            // Find where g_objCurrentFormData arrays are defined in javascript
            while (true)
            {
                varNameStart = body.IndexOf(VarNameStart, varNameStart, StringComparison.Ordinal);

                // Break out of the loop when there are no more matches
                if (varNameStart < 0)
                {
                    break;
                }

                varNameEnd = body.IndexOf(Equals, varNameStart, StringComparison.Ordinal);
                varName = ParseSubstring(body, varNameStart, varNameEnd - 1);

                // Extract the string between the brackets [...];
                int arrayStart = body.IndexOf(Equals, varNameEnd, StringComparison.Ordinal) + Equals.Length;
                int arrayEnd = body.IndexOf("];", arrayStart, StringComparison.Ordinal);
                string var = ParseSubstring(body, arrayStart, arrayEnd - 1);
                var = System.Text.RegularExpressions.Regex.Unescape(var);

                // Update the index to search for the next g_objCurrentFormData
                varNameStart = arrayEnd;

                // Create the CurrentFormData object and add it to the list
                InfoPathExtractionRules.CurrentFormData data = new InfoPathExtractionRules.CurrentFormData(var, varName);
                currentFormData.Add(data);
            }

            if (currentFormData.Count <= 0)
            {
                throw new ArgumentException("Did not find any g_ObjCurrentFormData array definitions in the string body");
            }

            return currentFormData;
        }

        /// <summary>
        /// Parses a javascript array declaration and returns an item from it.
        /// </summary>
        /// <param name="var">
        /// The string that would be on the RHS of a javascript array declaration
        /// and between brackets.
        /// </param>
        /// <param name="itemIndexIn">Index of the item in the array to return.</param>
        /// <returns>Item from "var" array at specified index.</returns>
        public static string ExtractItemFromJavascriptArray(string var, int itemIndexIn)
        {
            int startIndex = 0;
            int itemIndex = 0;
            int nesting = 0;
            char c;

            // Parse the items in the array
            // The items in the array are separated by commas,
            // but the array can contain other arrays which can contain commas
            for (int i = 0; i < var.Length; i++)
            {
                c = var[i];

                // Take into account arrays within this array
                if (c == '[')
                {
                    nesting++;
                }
                else if (c == ']')
                {
                    nesting--;
                }

                // If we are not in an internal array and
                // we reached a comma or the end of the string,
                // then we parsed an item
                if (nesting == 0 && (c == ',' || i == var.Length - 1))
                {
                    // If we parsed the item we were looking for,
                    // then return it
                    if (itemIndex == itemIndexIn)
                    {
                        string item = ParseSubstring(var, startIndex, i);
                        return item.Trim(',');
                    }

                    startIndex = i + 1;
                    itemIndex++;
                }
            }

            return null;
        }

        /// <summary>
        /// Gets a substring using a start and end index range.
        /// </summary>
        /// <param name="input">String to extract the substring from.</param>
        /// <param name="startIndex">First character in "str" where the substring begins.</param>
        /// <param name="endIndex">Last character in "str" where the substring ends.</param>
        /// <returns>Substring from "str" from startIndex to endIndex.</returns>
        public static string ParseSubstring(string input, int startIndex, int endIndex)
        {
            int length = endIndex - startIndex + 1;
            return input.Substring(startIndex, length);
        }

        /// <summary>
        /// Check if the request is an InfoPath invocation.
        /// </summary>
        /// <param name="request">The request to check.</param>
        /// <param name="invocationPath">The string used to identify an InfoPath invocation.</param>
        /// <returns>If the request is an InfoPath invocation.</returns>
        public static bool IsInvocation(WebTestRequest request, string invocationPath)
        {
            // Check if the request is using the GET transport method
            // and that the url is for an invocation
            return (request.Method == "GET") && request.Url.Contains(invocationPath);
        }

        /// <summary>
        /// Check if the request is an InfoPath partial postback.
        /// </summary>
        /// <param name="request">The request to check.</param>
        /// <returns>If the request is an InfoPath partial postback.</returns>
        public static bool IsPartialPostBack(WebTestRequest request)
        {
            // Check if the request is using the POST transport method
            // and that the url is for an postback
            return (request.Method == "POST") && request.Url.Contains(IPExtractionUtility.PartialPostBackPath);
        }

        /// <summary>
        /// Check if the request is an InfoPath full page postback.
        /// </summary>
        /// <param name="request">The request to check.</param>
        /// <param name="invocationPath">The string used to identify an InfoPath full page postback.</param>
        /// <returns>If the request is an InfoPath full page postback.</returns>
        public static bool IsFullPagePostBack(WebTestRequest request, string invocationPath)
        {
            // Check if the request is using the POST transport method
            // and that the url is for an invocation
            return (request.Method == "POST") && request.Url.Contains(invocationPath);
        }

        /// <summary>
        /// Substitutes the dynamic CurrentFormData extracted from an earlier request.
        /// </summary>
        /// <param name="body">String body of the InfoPath postback containing the event logs.</param>
        /// <param name="context">Context from an earlier request that contains an extracted CurrentFormData.</param>
        /// <param name="format">Format of the string body.</param>
        /// <returns>String body with the dynamic data substituted from the context.</returns>
        public static string SubstituteCurrentFormData(string body, WebTestContext context, Format format)
        {
            FormsServicesPostBackBody ipfsBody = new FormsServicesPostBackBody(body, format);
            Collection<CurrentFormData> currentFormData = context[CurrentFormData.ContextParameterName] as Collection<CurrentFormData>;
            ipfsBody.SubstituteCurrentFormData(currentFormData);

            return ipfsBody.ToString();
        }

        /// <summary>
        /// Extracts CurrentFormData from the response body string.
        /// </summary>
        /// <param name="response">Response from an InfoPath request.</param>
        /// <param name="context">Context to store the extracted CurrentFormData.</param>
        /// <returns>If the extraction was successful.</returns>
        public static bool ExtractCurrentFormData(WebTestResponse response, WebTestContext context)
        {
            Collection<CurrentFormData> currentFormData = IPExtractionUtility.GetCurrentFormDataArrays(response.BodyString);
            const string Key = CurrentFormData.ContextParameterName;

            if (context.ContainsKey(Key))
            {
                context[Key] = currentFormData;
            }
            else
            {
                context.Add(Key, currentFormData);
            }

            return true;
        }
    }
}
